/*
 *  repo-clean.cpp
 *
 *  Copyright (c) 2008 by Artyom Smirnov <smirnoffjr@gmail.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <string>
#include <alpm.h>
#include <libintl.h>
#include <errno.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/convenience.hpp>
#include <boost/foreach.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/version.hpp>
#include <iostream>
#include <stdio.h>

#include "config.hpp"

namespace bfs  = boost::filesystem;
namespace balg = boost::algorithm;

#define _(String) gettext (String)
#define foreach BOOST_FOREACH

struct version_item
{
    std::string pkg_version;
    std::string pkg_filename;
};

struct comparing_tree_item
{
    std::string pkg_name;
    std::vector<version_item> pkg_versions;
};

typedef std::vector<comparing_tree_item> comparing_tree;

struct arch_comparing_tree_item
{
    std::string arch;
    comparing_tree arch_comparing_tree;
};

typedef std::vector<arch_comparing_tree_item> arch_comparing_tree;

enum clean_mode
{
    MODE_DRY,
    MODE_MOVE,
    MODE_CLEAN
};

enum move_to_existing_mode
{
    MODE_ERROR,
    MODE_OVERWRITE,
    MODE_SKIP
};

enum messages_level
{
    MSG_SILENT,
    MSG_VERBOSE
};

#define VERBOSE msgs == MSG_VERBOSE
#define VERBOSE_PRINTF(...) if (VERBOSE) printf(__VA_ARGS__)

#define CONDABORT(condition)\
    if (condition) exit(EXIT_FAILURE);

#define CONDMSGABORT(condition, ...)\
    if (condition) { fprintf(stderr, __VA_ARGS__); exit(EXIT_FAILURE); }

#define MSGABORT(...)\
    fprintf(stderr, __VA_ARGS__); exit(EXIT_FAILURE);

#define MSGIGNOREABORT(...) \
    fprintf(stderr, __VA_ARGS__); if (!ignore_errors) exit(EXIT_FAILURE);

#ifdef DEBUG
#define DEBUG_PRINTF(...) fprintf(stderr, __VA_ARGS__)
#else
#define DEBUG_PRINTF(...)
#endif

#define BOOST_VERSION_MAJOR BOOST_VERSION / 100000
#define BOOST_VERSION_MINOR BOOST_VERSION / 100 % 1000
#define BOOST_VERSION_SUBMINOR BOOST_VERSION % 100

std::string package_suffix = ".pkg.tar.gz";
clean_mode mode = MODE_DRY;
move_to_existing_mode move_mode = MODE_ERROR;
messages_level msgs = MSG_VERBOSE;
string_list source_directories;
std::string move_directory = "";
string_list ignored;
bool ignore_errors = false;
bool create_directories = false;
bool recursive_search = false;

unsigned int get_last_version(std::vector<version_item>);
void pretty_print(arch_comparing_tree, bool);
void moving(comparing_tree, std::string);
void cleaning(comparing_tree);
void add_pkg_version(std::string, std::string, std::string, std::string, arch_comparing_tree&);
string_list build_pkg_list(std::string, bool);
int parse_args(int, char**);
bool is_ignored(std::string);
void help();
void version();

int main(int argc, char **argv)
{
    setlocale(LC_ALL, "");
#ifndef DEBUG
    bindtextdomain("repo-clean", INSTALL_PREFIX"/share/locale");
#else
    char self[4096];
    realpath("/proc/self/exe", self);
    std::string locale_dir = self;
    locale_dir = locale_dir.erase(locale_dir.find_last_of("/") + 1) + "../share/locale";
    bindtextdomain("repo-clean", locale_dir.c_str());
#endif
    textdomain("repo-clean");

    CONDABORT(parse_args(argc, argv));

    // check source existing
    foreach (std::string src, source_directories)
    {
        try
        {
            CONDMSGABORT(!bfs::exists(src), _("Source directory '%s' not exists.\n"),
                    src.c_str())
        }
        catch(...)
        {
            MSGABORT(_("Can not access directory '%s'.\n"), src.c_str());
        }
    }

    if (mode == MODE_MOVE)
    {
        try
        {
            // check destination directory existing
            CONDMSGABORT(!bfs::exists(move_directory) && !create_directories, _("Destination directory '%s' not exists.\n"),
                    move_directory.c_str());
        }
        catch(...)
        {
            MSGABORT(_("Can not access directory '%s'.\n"),
                    move_directory.c_str());
        }
        // check equivalence of source and destination
        // check source existing
        foreach (std::string src, source_directories)
        {
            CONDMSGABORT(bfs::equivalent(src, move_directory),
                    _("Source and destination directory can not be equivalent.\n"))
        }
    }

    Configuration conf;
    ignored = conf.GetIgnoreList();

    if (VERBOSE && ignored.size() > 0)
    {
        printf(_("Ignored packages:\n"));
        string_list::iterator item;
        for (item = ignored.begin(); item != ignored.end(); item++)
            printf("  %s\n", item->c_str());
    }

    // get all potential packages
    string_list list;
    foreach (std::string src, source_directories)
    {
        string_list l = build_pkg_list(src, recursive_search);
        list.insert(list.end(), l.begin(), l.end());
    }

    // build comp_tree
    arch_comparing_tree arch_comp_tree;
    int total = list.size(), err = 0, ign = 0;
    alpm_initialize();
    for (unsigned int i = 0; i < list.size(); i++)
    {
        // load package
        pmpkg_t *pkg= NULL;
        std::string path = list[i];
        if (alpm_pkg_load(path.c_str(), 0, &pkg) == 0)
        {
            std::string pkg_name = alpm_pkg_get_name(pkg);
            std::string pkg_version = alpm_pkg_get_version(pkg);
            std::string pkg_arch = alpm_pkg_get_arch(pkg) ? alpm_pkg_get_arch(pkg) : "unknown";

            // check for ignoring
            if (is_ignored(pkg_name))
            {
                ign++;
                continue;
            }

            // adding package to comp_tree
            VERBOSE_PRINTF(_("%cProcessing %d of %d..."), '\r', i + 1, (int)list.size());
            add_pkg_version(pkg_name, pkg_version, list[i], pkg_arch, arch_comp_tree);
            alpm_pkg_free(pkg);
        }
        else
        {
            fprintf(stderr, _("%cCan not load package '%s'. Corrupted?\n"), '\r',
                    list[i].c_str());
            err++;
        }
    }
    VERBOSE_PRINTF(_("%cTotal: %d  Ignored: %d  Corrupted: %d\n"), '\r', total, ign, err);
    alpm_release();

    // check if comp_tree contain multiversion packages
    bool found_multiversions = false;
    foreach (arch_comparing_tree_item item, arch_comp_tree)
    {
        foreach (comparing_tree_item tree_item, item.arch_comparing_tree)
        {
            if (tree_item.pkg_versions.size() > 1)
            {
                found_multiversions = true;
                break;
            }
        }
    }

    // if comp_tree doesn't contain packages, so nothing to do
    if (!found_multiversions)
    {
        VERBOSE_PRINTF(_("Can not find packages with several versions. Nothing to do.\n"));
        return EXIT_SUCCESS;
    }

    switch (mode)
    {
        case MODE_DRY:
            VERBOSE_PRINTF(_("Dry run mode...\nNewer packages marked by asterisk.\n"));
            pretty_print(arch_comp_tree, true);
            break;
        case MODE_MOVE:
            VERBOSE_PRINTF(_("Move mode...\n"));
            foreach (arch_comparing_tree_item item, arch_comp_tree)
                moving(item.arch_comparing_tree, move_directory);
            break;
        case MODE_CLEAN:
            VERBOSE_PRINTF(_("Clean mode...\n"));
            foreach (arch_comparing_tree_item item, arch_comp_tree)
                cleaning(item.arch_comparing_tree);
            break;
    }

    return EXIT_SUCCESS;
}

unsigned int get_last_version(std::vector<version_item> pkg_versions)
{
    //
    // Get array with versions package and return index of newer.
    //

    unsigned int last_version = 0;
    if (pkg_versions.size() > 1)
    {
        for (unsigned int i = 1; i < pkg_versions.size(); i++)
        {
            if (alpm_pkg_vercmp(pkg_versions[i].pkg_version.c_str(),
                    pkg_versions[last_version].pkg_version.c_str()) > 0)
                last_version = i;
        }
    }
    return last_version;
}

void pretty_print(arch_comparing_tree arch_comp_tree, bool only_multiversions)
{
    //
    // Prints packages of comp_tree with more than 1 versions and mark newest.
    //

    foreach (arch_comparing_tree_item item, arch_comp_tree)
    {
        for (unsigned int i = 0; i < item.arch_comparing_tree.size(); i++)
        {
            if (only_multiversions && item.arch_comparing_tree[i].pkg_versions.size() < 2)
                continue;

            printf("%s (%s)\n", item.arch_comparing_tree[i].pkg_name.c_str(), item.arch.c_str());

            unsigned int last_version_num =
                    get_last_version(item.arch_comparing_tree[i].pkg_versions);

            for (unsigned int j = 0; j < item.arch_comparing_tree[i].pkg_versions.size(); j++)
            {
                printf("  %c %s - %s\n", j == last_version_num ? '*' : ' ',
                        item.arch_comparing_tree[i].pkg_versions[j].pkg_version.c_str(),
                        item.arch_comparing_tree[i].pkg_versions[j].pkg_filename.c_str());
            }
        }
    }
}

void moving(comparing_tree comp_tree, std::string destination)
{
    //
    // Move all old packages to new destination.
    //

    try
    {
        // check move_directory existing
        if (!bfs::exists(destination))
        {
            // if not, but create_directories flag setted up, recreate all path
            if (create_directories)
            {
                // destination is absolute or relative path?
                std::string destination_trimmed = destination;
                balg::trim(destination_trimmed);
                char first = destination[0];
                std::string creating_directory = first == '/' ? "/" : "";

                // walk by subdirectories and create it
                VERBOSE_PRINTF(_("Creating destination directory '%s'...\n"), destination.c_str());
                string_list::iterator dir;
                string_list dirs;// = split(destination, "/");
                balg::split(dirs, destination, balg::is_any_of("/"));
                for (dir = dirs.begin(); dir != dirs.end(); dir++)
                {
                    creating_directory += *dir + "/";
                    try
                    {
                        // if this subdirectory exists, so skip it
                        if (bfs::exists(creating_directory))
                        {
                            continue;
                        }
                    }
                    catch(...)
                    {
                        MSGABORT(_("Can not access directory '%s'.\n"), creating_directory.c_str());
                    }

                    try
                    {
                        // create subdirectory
                        bfs::create_directory(creating_directory);
                    }
                    catch(...)
                    {
                        MSGABORT(_("Can not create directory '%s'.\n"), destination.c_str());
                    }
                }// for
            }
            else
            {
                MSGABORT(_("Destination directory '%s' not exists.\n"), destination.c_str());
            }// if(create_directories)
        }// if(!bfs::exists(destination))

        // check is existing destination - really directory
        CONDMSGABORT(!bfs::is_directory(destination), _("'%s' is not directory.\n"),
                destination.c_str());
    }
    catch(...)
    {
        MSGABORT(_("Can not access directory '%s'.\n"), destination.c_str());
    }

    for (unsigned int i = 0; i < comp_tree.size(); i++)
    {
        if (comp_tree[i].pkg_versions.size() < 2)
            continue;

        unsigned int last_version_num =
                get_last_version(comp_tree[i].pkg_versions);
        VERBOSE_PRINTF(
                    _("%s (keeping %s):\n"),
                    comp_tree[i].pkg_name.c_str(),
                    comp_tree[i].pkg_versions[last_version_num].pkg_filename.c_str());
        for (unsigned int j = 0; j < comp_tree[i].pkg_versions.size(); j++)
        {
            if (j != last_version_num)
            {
                std::string src = comp_tree[i].pkg_versions[j].pkg_filename;
                std::string dst = destination + "/" + bfs::basename(comp_tree[i].pkg_versions[j].pkg_filename)
                    + bfs::extension(comp_tree[i].pkg_versions[j].pkg_filename);

                // check destination existing
                bool exists = false;
                try
                {
                    exists = bfs::exists(dst);
                }
                catch(...)
                {
                    MSGIGNOREABORT(_("Can not check existence of file '%s'.\n"),
                            dst.c_str());
                }

                // abort by default if dst exists
                if (exists && move_mode == MODE_ERROR)
                {
                    MSGIGNOREABORT(_("File '%s' exists in destination already.\n"),
                            comp_tree[i].pkg_versions[j].pkg_filename.c_str());
                }
                else
                {
                    // skip existing dst
                    if (move_mode == MODE_SKIP)
                    {
                        VERBOSE_PRINTF(_("  skipping %s\n"),
                                    comp_tree[i].pkg_versions[j].pkg_filename.c_str());
                        continue;
                    }
                    // remove destination before move (overwrite)
                    else
                    {
                        try
                        {
                            bfs::remove(dst);
                        }
                        catch(...)
                        {
                            MSGIGNOREABORT(_("Can not overwrite file '%s'\n"),
                                    dst.c_str());
                        }
                    }
                }

                VERBOSE_PRINTF(_("  moving %s\n"),
                            comp_tree[i].pkg_versions[j].pkg_filename.c_str());
                // trying to move file to new destination
                try
                {
                    bfs::rename(src, dst);
                }
                catch(bfs::filesystem_error const &e)
                {
                    // if we fail with EXDEV, so try to copy-remove file

#if BOOST_VERSION_MAJOR <= 1 and BOOST_VERSION_MINOR <= 34
                    if (e.system_error() == EXDEV)
#else
                    if (e.code() == boost::system::posix_error::cross_device_link)
#endif
                    {
                        try
                        {
                            bfs::copy_file(src, dst);
                            bfs::remove(src);
                        }
                        catch(...)
                        {
                            MSGIGNOREABORT(_("Can not move file '%s'\n"),
                                    comp_tree[i].pkg_versions[j].pkg_filename.c_str());
                        }
                    }
                    else
                    {
                        MSGIGNOREABORT(_("Can not move file '%s'\n"),
                                comp_tree[i].pkg_versions[j].pkg_filename.c_str());
                    }
                }
            }
        }
    }
}

void cleaning(comparing_tree comp_tree)
{
    //
    // Permanently remove all old packages
    //

    for (unsigned int i = 0; i < comp_tree.size(); i++)
    {
        if (comp_tree[i].pkg_versions.size() < 2)
            continue;

        unsigned int last_version_num =
                get_last_version(comp_tree[i].pkg_versions);
        VERBOSE_PRINTF(
                    _("%s (keeping %s):\n"),
                    comp_tree[i].pkg_name.c_str(),
                    comp_tree[i].pkg_versions[last_version_num].pkg_filename.c_str());
        for (unsigned int j = 0; j < comp_tree[i].pkg_versions.size(); j++)
        {
            if (j != last_version_num)
            {
                VERBOSE_PRINTF(_("  deleting %s\n"),
                            comp_tree[i].pkg_versions[j].pkg_filename.c_str());

                std::string src = comp_tree[i].pkg_versions[j].pkg_filename;

                try
                {
                    bfs::remove(src);
                }
                catch(...)
                {
                    MSGIGNOREABORT(_("Can not remove file '%s'\n"), src.c_str());
                }
            }
        }
    }
}

void add_pkg_version(std::string pkg_name, std::string pkg_version, std::string pkg_filename,
        std::string pkg_arch, arch_comparing_tree &arch_comp_tree)
{
    //
    // Add package in comp_tree if package with name pkg_name not present in array,
    // otherwise add version of this package
    //

    bool pkg_exists = false;
    bool arch_exists = false;
    unsigned int pkg_num = 0;
    unsigned int arch_num = 0;

    // check arch existing
    int i = 0;
    foreach (arch_comparing_tree_item item, arch_comp_tree)
    {
        if (item.arch == pkg_arch)
        {
            arch_exists = true;
            arch_num = i;
            break;
        }
        i++;
    }

    // check package existens in comp_tree
    if (arch_exists)
    {
        for (unsigned int i = 0; i < arch_comp_tree[arch_num].arch_comparing_tree.size(); i++)
        {
            if (arch_comp_tree[arch_num].arch_comparing_tree[i].pkg_name == pkg_name)
            {
                pkg_exists = true;
                pkg_num = i;
                break;
            }
        }
    }

    version_item vitem;
    vitem.pkg_filename = pkg_filename;
    vitem.pkg_version = pkg_version;
    if (pkg_exists)
    {
        // add version to existing package
        arch_comp_tree[arch_num].arch_comparing_tree[pkg_num].pkg_versions.push_back(vitem);
    }
    else
    {
        // add package to comp_tree
        comparing_tree_item ctitem;
        ctitem.pkg_name = pkg_name;
        ctitem.pkg_versions.push_back(vitem);
        if (arch_exists)
        {
            arch_comp_tree[arch_num].arch_comparing_tree.push_back(ctitem);
        }
        else
        {
            // add arch to comp_tree
            arch_comparing_tree_item arch_comp_tree_item;
            arch_comp_tree_item.arch = pkg_arch;
            arch_comp_tree_item.arch_comparing_tree.push_back(ctitem);
            arch_comp_tree.push_back(arch_comp_tree_item);
        }
    }
}

string_list build_pkg_list(std::string source_directory, bool recursive)
{
    //
    // Search files with extension .pkg.tar.gz in source directory and add them
    // to string list.
    //

    string_list list;

    try
    {
        if (bfs::is_directory(source_directory))
        {
            bfs::directory_iterator end_iter;
            for (bfs::directory_iterator dir_iter(source_directory); dir_iter != end_iter; dir_iter++)
            {
                if (bfs::is_directory(dir_iter->status()) && recursive)
                {
#if BOOST_VERSION_MAJOR <= 1 and BOOST_VERSION_MINOR < 36
                    string_list sublist = build_pkg_list(source_directory + "/" + dir_iter->leaf(),
                            recursive);
#else
                    string_list sublist = build_pkg_list(source_directory + "/" + dir_iter->filename(),
                            recursive);
#endif
                    list.insert(list.end(), sublist.begin(), sublist.end());
                }
                else if (bfs::is_regular(dir_iter->status()) || bfs::is_symlink(dir_iter->status()))
                {
#if BOOST_VERSION_MAJOR <= 1 and BOOST_VERSION_MINOR < 36
                    std::string name = dir_iter->leaf();
#else
                    std::string name = dir_iter->filename();
#endif
                    if ((int)name.find(package_suffix) > 0 && (name.length()
                            - name.find(package_suffix) == package_suffix.length()))
                    {
                        list.push_back(source_directory + "/" + name);
                    }
                }
            }
        }
    }
    catch(...)
    {
        MSGABORT(_("Error when reading source directory '%s'\n"),
                source_directory.c_str());
    }

    return list;
}

bool is_ignored(std::string pkg_name)
{
    //
    // Check is package in ignore list.
    //

    for (unsigned int i = 0; i < ignored.size(); i++)
    {
        if (ignored[i] == pkg_name)
            return true;
    }
    return false;
}

int parse_args(int argc, char **argv)
{
    //
    // Command line arguments parsing.
    //

    char c;
    opterr = 0;
    bool move_directory_defined = false;
    bool source_directory_defined = false;
    while ((c = getopt(argc, argv, "hqm:d:s:fvce:r")) != -1)
    {
        switch (c)
        {
            case 'h':
                help();
                exit(EXIT_SUCCESS);

            case 'q':
                msgs = MSG_SILENT;
                break;

            case 'm':
                switch (optarg[0])
                {
                    case 'd':
                        mode = MODE_DRY;
                        break;

                    case 'm':
                        mode = MODE_MOVE;
                        break;

                    case 'c':
                        mode = MODE_CLEAN;
                        break;

                    default:
                        fprintf(stderr,
                                _("Unknown argument '%s' for option - '%c'.\n"),
                                optarg, c);
                        return 1;
                }
                break;

            case 'd':
                move_directory = optarg;
                move_directory_defined = true;
                break;

            case 's':
                foreach (std::string src, source_directories)
                    CONDMSGABORT(bfs::equivalent(src, optarg), _("All sources must be different.\n"));
                source_directories.push_back(optarg);
                source_directory_defined = true;
                break;

            case 'f':
                ignore_errors = true;
                break;

            case 'v':
                version();
                exit(EXIT_SUCCESS);

            case 'c':
                create_directories = true;
                break;

            case 'e':
                switch (optarg[0])
                {
                    case 'o':
                        move_mode = MODE_OVERWRITE;
                        break;

                    case 's':
                        move_mode = MODE_SKIP;
                        break;

                    default:
                        fprintf(stderr,
                                _("Unknown argument '%s' for option -%c.\n"),
                                optarg, c);
                        return 1;
                }
                break;

            case 'r':
                recursive_search = true;
                break;

            case '?':
                if (optopt == 'm' || optopt == 'd' || optopt == 's' || optopt == 'c'
                    || optopt == 'e')
                    fprintf(stderr, _("Option -%c requires an argument.\n"),
                            optopt);
                else if (isprint(optopt))
                    fprintf(stderr, _("Unknown option `-%c'.\n"), optopt);
                else
                    fprintf(stderr, _("Unknown option character `\\x%x'.\n"),
                            optopt);
                return 1;

            default:
                abort();
        }
    }

    if (mode == MODE_MOVE && !move_directory_defined)
    {
        fprintf(stderr, _("You must define directory for moving old packages!\n"));
        return 1;
    }

    if (!source_directory_defined)
    {
        source_directories.push_back(".");
        VERBOSE_PRINTF(_("Source directory not defined! Current directory will used as source.\n"));
    }

    // check if one source directory overlap other
    if (recursive_search && source_directories.size() > 1)
    {
        for (unsigned int i = 0; i < source_directories.size(); i++)
        {
            for (unsigned int j = i + 1; j < source_directories.size(); j++)
            {
                CONDMSGABORT(source_directories[i].find(source_directories[j]) == 0 ||
                        source_directories[j].find(source_directories[i]) == 0,
                        _("Check sources list, you have crossed pathes (%s and %s)!\n"),
                        source_directories[i].c_str(), source_directories[j].c_str());
            }
        }
    }

    return 0;
}

void help()
{
    printf(_("Using: repo-clean [-s source1 [-s source2] ...] [-m<d|m [-d destination]|c>] [-f] [-c] [-e<o|s>] [-r] [-q]\n"
            "-h - this text\n"
        "-q - silent mode\n"
        "-m - processing mode\n"
        "     d - dry mode (default)\n"
        "     m - move mode\n"
        "     c - clean mode\n"
        "-s - source directory (default .)\n"
        "-d - destination directory (for move mode)\n"
        "-f - force continue on errors when move or clean\n"
        "-c - create destination directory (if not exists)\n"
        "-e - action when move to existing file (default abort with error)\n"
        "     o - overwrite\n"
        "     s - skip\n"
        "-r - recursive packages search\n"
        "-v - version\n"));
}

void version()
{
    printf("repo-clean v%s\n"
           "Copyright (C) 2008-2009 Artyom Smirnov <smirnoffjr@gmail.com>\n\n"
           "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n"
           "This is free software: you are free to change and redistribute it.\n"
           "There is NO WARRANTY, to the extent permitted by law.\n",
           (char*)VERSION);
}
